require_relative 'actions'

module RedisHashStore
  module RailsCache
    extend self

    def redis_hash_cache(*objects, expire_when_updated: true, force_update: false)
      if self == RedisHashStore::RailsCache
        raise "Please don't call #{self}.#{__method__} directly but include #{self} in a class or module and call #{__method__} from within."
      end

      if objects.all?(&:persisted?) && perform_caching
        key_parts = objects
          .map { |object| format_for_key(object.class) }
          .concat(objects[0..-2].map(&:id))
          .unshift(format_for_key(self.class))
        number = objects.last.id
        field_prefix = objects.map(&:id).join(':')
        field = if expire_when_updated
          objects
            .map { |object| object.updated_at.strftime('%Y%m%d%H%M%S') }
            .unshift(field_prefix)
            .join(':')
        else
          field_prefix
        end

        unless force_update
          if value = RedisHashStore.get(key_parts, number, field)
            return deserialize(value)
          end
        end

        if expire_when_updated || force_update
          RedisHashStore.del key_parts, number, /\A#{field_prefix}\b/
        end

        value = yield

        RedisHashStore.save key_parts, number, serialize(value), field

        value
      else
        yield
      end
    end

    private

    def perform_caching
      Rails.application.config.action_controller.perform_caching
    end

    def format_for_key(object)
      object.to_s.tr(':', '_')
    end

    def serialize(value)
      case value
      when String
        value
      when Array, Hash
        value.to_json
      else
        raise "Cannot serialize #{value.class}, must be string, array or hash."
      end
    end

    def deserialize(value)
      JSON.parse(value) rescue value
    end
  end
end
